/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.lvyh.lightframe.tracer.plugin.springcloud;


import com.lvyh.lightframe.tracer.core.SimpleSpan;
import com.lvyh.lightframe.tracer.core.SimpleTracer;
import com.lvyh.lightframe.tracer.core.common.AbstractTracer;
import com.lvyh.lightframe.tracer.core.common.SimpleTracerConfiguration;
import com.lvyh.lightframe.tracer.core.common.TraceContants;
import com.lvyh.lightframe.tracer.core.context.SimpleTraceContext;
import com.lvyh.lightframe.tracer.core.context.SimpleTraceContextHolder;
import com.lvyh.lightframe.tracer.core.samplers.Sampler;
import com.lvyh.lightframe.tracer.core.util.StringUtils;
import com.lvyh.lightframe.tracer.plugin.springcloud.carriers.FeignRequestCarrier;
import io.opentracing.propagation.Format;
import io.opentracing.tag.Tags;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import okio.Buffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class OkHttpFeignInterceptor implements Interceptor {
    private static final Logger logger = LoggerFactory.getLogger(OkHttpFeignInterceptor.class);

    private AbstractTracer feignClientTracer;

    public OkHttpFeignInterceptor(AbstractTracer feignClientTracer) {
        this.feignClientTracer = feignClientTracer;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        logger.info("[OkHttpFeignInterceptor] entry in OkHttp interceptor.");

        Request request = chain.request();
        SimpleSpan simpleSpan = null;
        int resultCode = -1;

        String url = request.url().url().toString();
        String method = request.method();
        Map<String, Collection<String>> headers = this.convertFeignRequest(request);
        byte[] body = new byte[1];
        if ("POST".equals(method)) {
            String s = this.bodyToString(request);
            body = s.getBytes();
        }

        // public static Request create(String method, String url, Map<String, Collection<String>> headers, byte[] body, Charset charset) {
        feign.Request feignRequest = feign.Request.create(method, url, headers, body, Charset.defaultCharset());

        try {
            simpleSpan = feignClientTracer.clientSend(request.method());
            setSpanTagsAndInject(feignRequest, simpleSpan);
            Response response = chain.proceed(request);
            simpleSpan.setTag(TraceContants.RESP_SIZE, response.body().contentLength());
            simpleSpan.setTag(TraceContants.CURRENT_THREAD_NAME, Thread.currentThread().getName());
            simpleSpan.setTag(TraceContants.RESULT_CODE, response.code());

            resultCode = response.code();

            logger.info("[OkHttpFeignInterceptor] exit OkHttp interceptor.");
            return response;
        } catch (Exception e) {
            setSpanTags(feignRequest, simpleSpan);
            simpleSpan.setTag(Tags.ERROR.getKey(), e.getMessage());
            throw new RuntimeException(e);

        } finally {
            if (simpleSpan != null) {
                SimpleTraceContext simpleTraceContext = SimpleTraceContextHolder.getSimpleTraceContext();
                SimpleSpan currentSpan = simpleTraceContext.getCurrentSpan();
                Sampler sampler = feignClientTracer.getSimpleTracer().getSampler();
                //sampler is support &  current span is root span
                if (sampler != null && currentSpan.getParentSpan() == null) {
                    currentSpan.getSimpleSpanContext().setSampled(true);
                    currentSpan.getSimpleSpanContext().setSampler(sampler);
                }

                feignClientTracer.clientReceive(String.valueOf(resultCode));
            }
        }
    }

    private static String bodyToString(final Request request) {
        try {
            final Request copy = request.newBuilder().build();
            final Buffer buffer = new Buffer();
            copy.body().writeTo(buffer);
            return buffer.readUtf8();
        } catch (final IOException e) {
            return "error";
        }
    }


    private void setSpanTagsAndInject(feign.Request request, SimpleSpan simpleSpan) {
        setSpanTags(request, simpleSpan);
        try {
            this.injectCarrier(request, simpleSpan);
        } catch (UnsupportedOperationException e) {
            Map<String, Collection<String>> headers = request.headers();
            request = feign.Request.create(request.method(), request.url(), new LinkedHashMap<>(headers),
                    request.body(), request.charset());
            injectCarrier(request, simpleSpan);
        }
    }

    private Map<String, Collection<String>> convertFeignRequest(Request request) {
        Map<String, Collection<String>> headers = new LinkedHashMap<String, Collection<String>>();
        Headers requestHeaders = request.headers();
        for (int i = 0; i < requestHeaders.size(); i++) {
            String headerName = requestHeaders.name(i);
            String headerValue = requestHeaders.get(headerName);
            List<String> list = new ArrayList<>();
            list.add(headerValue);
            headers.put(headerName, list);
        }
        return headers;
    }

    private void setSpanTags(feign.Request request, SimpleSpan simpleSpan) {
        if (simpleSpan == null) {
            return;
        }
        simpleSpan.setTag(TraceContants.LOCAL_APP, SimpleTracerConfiguration.getApplicationName());
        simpleSpan.setTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT);
        simpleSpan.setTag(TraceContants.REMOTE_APP, StringUtils.EMPTY_STRING);
        simpleSpan.setTag(TraceContants.REQUEST_URL, request.url());
        simpleSpan.setTag(TraceContants.METHOD, request.method());

        String[] hostWithPort = parseRemoteHostAndPort(request);
        simpleSpan.setTag(TraceContants.REMOTE_HOST, hostWithPort[0]);
        simpleSpan.setTag(TraceContants.REMOTE_PORT, hostWithPort[1]);

        if (request.body() != null) {
            simpleSpan.setTag(TraceContants.REQ_SIZE, request.body().length);
        } else {
            simpleSpan.setTag(TraceContants.REQ_SIZE, 0);
        }
    }


    private void injectCarrier(Object request, SimpleSpan currentSpan) {
        if (request instanceof feign.Request) {
            SimpleTracer sofaTracer = this.feignClientTracer.getSimpleTracer();
            sofaTracer.inject(currentSpan.getSimpleSpanContext(),
                    Format.Builtin.HTTP_HEADERS, new FeignRequestCarrier((feign.Request) request));
        }
    }

    private String[] parseRemoteHostAndPort(feign.Request request) {
        String[] hostWithPort = new String[2];
        URL requestUrl = null;
        try {
            requestUrl = new URL(request.url());
        } catch (Exception e) {
            logger.error("cannot parse remote host and port. request:" + request.url(), e);
        }
        hostWithPort[0] = requestUrl != null ? requestUrl.getHost() : "";
        hostWithPort[1] = String.valueOf(requestUrl != null ? requestUrl.getPort() : -1);
        return hostWithPort;
    }

}
